package com.zzyl.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.db.nosql.redis.RedisDS;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.zzyl.base.PageResponse;
import com.zzyl.base.ResponseResult;
import com.zzyl.constant.CacheConstants;
import com.zzyl.constant.Constants;
import com.zzyl.constant.SuperConstant;
import com.zzyl.dto.AlertDataPageQueryDto;
import com.zzyl.dto.AlertNotifyDto;
import com.zzyl.dto.AlertRuleDto;
import com.zzyl.dto.AlertRulePageQueryDto;
import com.zzyl.entity.AlertData;
import com.zzyl.entity.AlertRule;
import com.zzyl.enums.BasicEnum;
import com.zzyl.exception.BaseException;
import com.zzyl.mapper.AlertDataMapper;
import com.zzyl.mapper.AlertRuleMapper;
import com.zzyl.mapper.DeviceMapper;
import com.zzyl.mapper.UserRoleMapper;
import com.zzyl.service.AlertRuleService;
import com.zzyl.utils.StringUtils;
import com.zzyl.vo.AlertRuleVo;
import com.zzyl.vo.DeviceDataVo;
import com.zzyl.websocket.WebSocketConfig;
import com.zzyl.websocket.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.implementation.bytecode.Division;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

@Service
@Slf4j
public class AlertRuleServiceImpl implements AlertRuleService {

    @Autowired
    private AlertRuleMapper alertRuleMapper;

    @Override
    public ResponseResult<AlertRule> findById(Long id) {
        AlertRule byId = alertRuleMapper.findById(id);
        return ResponseResult.success(byId);
    }

    @Override
    public List<AlertRule> findAll() {
        return alertRuleMapper.findAll();
    }

    /**
     * 新增报警规则
     */
    @Override
    public void insert(AlertRuleDto alertRuleDto) {
        // 首先进行属性拷贝
        AlertRule alertRule = BeanUtil.copyProperties(alertRuleDto, AlertRule.class);
        alertRuleMapper.insert(alertRule);
    }

    /**
     * 更新报警规则
     */
    @Override
    public void update(AlertRule alertRule) {
        alertRuleMapper.update(alertRule);
    }

    /**
     * 删除告警规则
     */
    @Override
    public void delete(Long id) {
        AlertRule alertRule = alertRuleMapper.findById(id);
        // 判断告警规则是否启用，如果启用，不能删除。
        if (alertRule.getStatus().equals(SuperConstant.STATUS_ENABLE)) {
            throw new BaseException(BasicEnum.ENABLED_CANNOT_DELETED);
        }
        alertRuleMapper.delete(id);
    }

    /**
     * 分页获取告警规则
     */
    @Override
    public ResponseResult<PageResponse<AlertRuleVo>> getPage(AlertRulePageQueryDto dto) {
        PageHelper.startPage(dto.getPageNum(), dto.getPageSize());
        // 正常查询
        Page<AlertRuleVo> alertRuleVoList = alertRuleMapper.findAllByCondition(dto);

        // 填充报警规则的提示
        alertRuleVoList.forEach(alertRuleVo -> {
            String alertReason = CharSequenceUtil.format("{},{},{},持久{}个周期就报警", alertRuleVo.getFunctionName(), alertRuleVo.getOperator(), alertRuleVo.getValue(), alertRuleVo.getDuration());
            alertRuleVo.setRules(alertReason);
        });


        // 封装返回的分页数据
        PageResponse<AlertRuleVo> pageResponse = PageResponse.of(alertRuleVoList, AlertRuleVo.class);
        return ResponseResult.success(pageResponse);
    }

    /**
     * 启用或禁用告警规则
     */
    @Override
    public void startOrStop(Integer id, Integer status) {
        AlertRule alertRule = new AlertRule();
        alertRule.setId(Long.valueOf(id));
        alertRule.setStatus(status);
        alertRuleMapper.update(alertRule);
    }

    /**
     * 校验redis中，设备上报的每一条数据，进行报警规则过滤处理
     */
    @Override
    public void alertFilter(DeviceDataVo deviceDataVo) {
        // 获取设备上报数据的时间，如果上报发生在一分钟前，为历史数据，不再处理
        LocalDateTime alarmTime = deviceDataVo.getAlarmTime();
        // 获取两个时间的时间差
        long between = LocalDateTimeUtil.between(alarmTime, LocalDateTime.now(), ChronoUnit.SECONDS);
        if (between > 60) {
            // 数据上报时间和当前时间相差大于60s，为过时的历史数据，直接结束，不再处理。
            return;
        }

        // 查询报警规则，将对于 该产品下拥有该功能的全部设备的报警规则 和对于 本设备的报警规则 进行合并
        // -1 是我们新增报警规则的时候定义的，-1代表全部设备 getProductKey 所属产品的key  getFunctionId功能ID
        List<AlertRule> allDeviceAlterRules = alertRuleMapper.selectByFunctionId(deviceDataVo.getFunctionId(), "-1", deviceDataVo.getProductKey());
        List<AlertRule> theDeviceAlterRules = alertRuleMapper.selectByFunctionId(deviceDataVo.getFunctionId(), deviceDataVo.getIotId(), deviceDataVo.getProductKey());
        Collection<AlertRule> alertRules = CollUtil.addAll(allDeviceAlterRules, theDeviceAlterRules);
        // 如果报警规则为空，程序结束
        if (CollectionUtil.isEmpty(alertRules)) {
            return;
        }

        // 如果报警规则不为空，遍历报警规则，进行校验
        alertRules.forEach(alertRule -> {
            // 抽出来一个方法，把一条设备数据，和所有的报警规则进行判断
            deviceDateAlarmHandler(deviceDataVo, alertRule);
        });
    }

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private DeviceMapper deviceMapper;

    @Autowired
    private UserRoleMapper userRoleMapper;

    //设备维护人员的角色名称
    @Value("${zzyl.alert.deviceMaintainerRole}")
    private String deviceMaintainerRole;
    //超级管理员的角色名称
    @Value("${zzyl.alert.managerRole}")
    private String managerRole;

    /**
     * 设备数据报警处理器：把一条设备数据，和所有的报警规则进行判断
     */
    private void deviceDateAlarmHandler(DeviceDataVo deviceDataVo, AlertRule alertRule) {
        // 第一层校验，判断报警规则是否在生效时间内。不在我们规定的报警时间，自然就不用报警啦
        // ~ 是前端定义的。选的一个时间范围
        String[] aepArr = alertRule.getAlertEffectivePeriod().split("~");
        // 字符串转 LocalTime
        LocalTime startTime = LocalTime.parse(aepArr[0]);
        LocalTime endTime = LocalTime.parse(aepArr[1]);
        // 数据上报时间  LocalDateTime 转 LocalTime
        LocalTime alertTime = LocalDateTimeUtil.of(deviceDataVo.getAlarmTime()).toLocalTime();
        // 如果上报时间不在有效的时间段内，校验结束
        if (startTime.isAfter(alertTime) || endTime.isBefore(alertTime)) {
            return;
        }

        // 获取到设备id，备用
        String iotId = deviceDataVo.getIotId();
        // 从redis中获取数据，拿到报警规则中的连续触发次数rediskey。
        String aggCountKey = CacheConstants.ALERT_TRIGGER_COUNT_PREFIX + iotId + ":" + deviceDataVo.getFunctionId() + ":" + alertRule.getId();

        // 第二层校验, 判断数据阀值
        // NumberUtil.compare(number1,number2) 判断规则
        // number1==number2 返回0; number1 < number2返回小于0的数; number1 > number2 返回大于0的数
        int compareResult = NumberUtil.compare(Double.parseDouble(deviceDataVo.getDataValue()), alertRule.getValue());
        // 或 逻辑运算。 2个条件满足一个就为true，数据达到阀值，是报警数据。
        // 1、运算符为 >=，设备上报数据  >= 阀值， compareResult >= 0
        // 2、运算符为 < , 设备上报数据  < 阀值，  compareResult < 0
        if (ObjectUtil.equals(alertRule.getOperator(), ">=") && compareResult >= 0 || ObjectUtil.equals(alertRule.getOperator(), "<") && compareResult < 0) {
            log.debug("JLGP:设备上报数据达到阀值，为报警数据！！！！");
        } else {
            // 这条数据不符合报警规则，所以设备上报数据是正常的数据，需要删除redis聚合异常的数据，程序结束
            redisTemplate.delete(aggCountKey);
        }

        // 第三层校验：校验当前设备+功能的报警规则是否处于沉默周期。比如定义闹铃，此时响啦，我们点了推迟一段时间，那么推迟的时间就是延迟周期。
        // 获取沉默周期的key
        String silentCacheKey = CacheConstants.ALERT_SILENT_PREFIX + iotId + ":" + deviceDataVo.getFunctionId() + ":" + alertRule.getId();
        // 获取沉默周期的数据值
        String silentData = redisTemplate.opsForValue().get(silentCacheKey);
        // 如果redis中存在对应的沉默周期的数据，则当前设备 + 功能正处于沉默周期，无需报警
        if (StringUtils.isNotEmpty(silentData)) {
            // 非空，说明有沉默周期的数据，报警时间没有到直接结束
            return;
        }

        // 第四层校验: 判断持续周期。 持久周期就是闹铃正在响，响多久再重新响。一个周期一分钟，我们这里aggCountData+1，就相当于加了一周期（一分钟），是因为我们定时任务一分钟执行一次
        // aggCountData是上次触发报警累计的次数，这一次又触发了报警，所以累积到这次就+1

        //4.1 获取连续触发报警规则的次数
        // aggCountData是上次触发报警累计到的次数，这一次又触发了报警，所以累计到这次就+1
        String aggCountData = redisTemplate.opsForValue().get(aggCountKey);
        int triggerCount = ObjectUtil.isEmpty(aggCountData) ? 1 : Integer.parseInt(aggCountData) + 1;

        //4.2 判断次数是否与持续周期相等
        // 如果触发报警规则次数不等于持续周期，则无需报警
        if (ObjectUtil.notEqual(alertRule.getDuration(), triggerCount)) {
            redisTemplate.opsForValue().set(aggCountKey, triggerCount + "");
            return;
        }

        // 如果触发报警规则次数等于持续周期，则需要报警完成以下操作
        //1) 删除报警规则连续触发次数的缓存
        //2) 添加对应的沉默周期，设置过期时间，添加对应的沉默周期，设置过期时间
        //3) 报警数据存储到主句哭中
        redisTemplate.delete(aggCountKey);
        redisTemplate.opsForValue().set(silentCacheKey, deviceDataVo.getDataValue(), alertRule.getAlertSilentPeriod(), TimeUnit.MINUTES);

        // 获取消息的消费者
        List<Long> consumerIds = null;
        // 前端指定，是否是老人的异常数据。 报警数据类型： 0 老人异常数据 1：设备异常数据
        if (ObjectUtil.equals(0, alertRule.getAlertDataType())) {
            // 如果是老人的异常数据，需要通知绑定老人的处理员，根据IotID查询老人绑定的护理员。
            // 位置类型：0 随身设备  1：固定设备
            if (deviceDataVo.getLocationType() == 0) {
                // 随身设备IotID查询老人，查询绑定的护理员。
                consumerIds = deviceMapper.selectNursingIdsByIotIdWithElder(iotId);
            } else {
                // 固定设备，根据设备找床位，根据床位找老人，根据老人找护理员
                consumerIds = deviceMapper.selectNursingIdsByIotIdWithBed(iotId);
            }
        } else {
            // 如果是设备的异常数据，需要通知到设备维护人员，根据指定的角色名称查询相关的用户
            consumerIds = userRoleMapper.selectUserIdsByRoleName(deviceMaintainerRole);
        }

        // 查询超级管理员，超级管理员不论什么消息都会接收
        List<Long> managerIds = userRoleMapper.selectUserIdsByRoleName(managerRole);
        List<Long> allConsumerIds = CollUtil.addAllIfNotContains(consumerIds, managerIds);
        allConsumerIds = CollUtil.distinct(allConsumerIds);

        // 新增报警数据
        List<AlertData> list = insertAlertData(deviceDataVo, alertRule, allConsumerIds);

        // 获取需要推送的用户ID列表
        List<Long> userIds = list.stream().map(AlertData::getUserId).collect(Collectors.toList());

        // 发送websocket 消息
        websocketNotify(alertRule, list.get(0), userIds);


    }

    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 发送websocket通知
     */
    private void websocketNotify(AlertRule alertRule, AlertData alertData, List<Long> userIds) {
        // websocket通知：弹窗消息
        AlertNotifyDto alertNotifyDto = BeanUtil.toBean(alertData, AlertNotifyDto.class);
        alertNotifyDto.setFunctionName(alertRule.getFunctionName());
        alertNotifyDto.setAlertDataType(alertData.getType());
        // 1 是报警 0 是接触报警
        alertNotifyDto.setNotifyType(1);
        // 给指定消费者发送消息 服务器给客户端发，所以用dto
        webSocketServer.sendMessageToConsumer(alertNotifyDto, userIds);
    }

    @Autowired
    private AlertDataMapper alertDataMapper;

    /**
     * 新增报警器
     */
    private List<AlertData> insertAlertData(DeviceDataVo deviceDataVo, AlertRule alertRule, List<Long> allConsumerIds) {
        // 格式化报警原因，数据库中有体现。
        // 报警原因，格式： 功能名称+运算符+阀值+持续周期+聚合周期
        String alertReson = CharSequenceUtil.format("{}{}{},持续了{}个周期就报警", alertRule.getFunctionName(), alertRule.getOperator(), alertRule.getValue(), alertRule.getDuration());
        AlertData alertData = BeanUtil.toBean(deviceDataVo, AlertData.class);
        alertData.setAlertRuleId(alertRule.getId());
        alertData.setAlertReason(alertReson);
        alertData.setType(alertRule.getAlertDataType());
        alertData.setStatus(0);
        // 根据消费者，需要通知的人，生成相应的报警数据
        List<AlertData> list = allConsumerIds.stream().map(id -> {
            AlertData dbAlertData = BeanUtil.toBean(alertData, AlertData.class);
            dbAlertData.setUserId(id);
            return dbAlertData;
        }).collect(Collectors.toList());

        // 插入数据
        alertDataMapper.batchInsert(list);

        // 返回插入的报警人数据
        return list;
    }
}